Skip to content

Conversation

@LucaButBoring
Copy link
Contributor

@LucaButBoring LucaButBoring commented Oct 23, 2025

This PR implements the required changes for modelcontextprotocol/modelcontextprotocol#1686, which adds task augmentation to requests.

Motivation and Context

The current MCP specification supports tool calls that execute a request and eventually receive a response, and tool calls can be passed a progress token to integrate with MCP’s progress-tracking functionality, enabling host applications to receive status updates for a tool call via notifications. However, there is no way for a client to explicitly request the status of a tool call, resulting in states where it is possible for a tool call to have been dropped on the server, and it is unknown if a response or a notification may ever arrive. Similarly, there is no way for a client to explicitly retrieve the result of a tool call after it has completed — if the result was dropped, clients must call the tool again, which is undesirable for tools expected to take minutes or more. This is particularly relevant for MCP servers abstracting existing workflow-based APIs, such as AWS Step Functions, Workflows for Google Cloud, or APIs representing CI/CD pipelines, among other applications.

This proposal (and implementation) solves this by introducing the concept of Tasks, which are pending work objects that can augment any other request. Clients generate a task ID and augment their request with it — that task ID is both a reference to the request and an idempotency token. If the server accepts the task augmentation request, clients can then poll for the status and eventual result of the task with the tasks/get and tasks/result operations.

How Has This Been Tested?

Unit tests and updated sHTTP example.

Breaking Changes

None.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Copy link

@halter73 halter73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SEP-1686 currently states that it "introduces a mechanism for requestors (which can be either clients or servers, depending on the direction of communication) to augment their requests with tasks." Is this still the case?

I don't see any tests or examples demonstrating using a TaskStore with an McpClient (it's all McpServer or the Protocol base type), although I suppose it should work considering its shared code. It still might be nice to have an end-to-end test demonstrating that client-side support for stuff like elicitations works using the public APIs.

* Note: This is not suitable for production use as all data is lost on restart.
* For production, consider implementing TaskStore with a database or distributed cache.
*/
export class InMemoryTaskStore implements TaskStore {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we cannot create a default in-memory task store with settable max limits on the total number of tasks?

I like that this PR adds the TaskStore abstraction which opens the door for a distributed implementation and doesn't require the application developer to manually handle tasks/get, tasks/list, etc., but it feels weird to me to not have a production-ready in-memory solution provided by the SDK.

It should still be off by default to make sure people are explicit about any maxTrackedTask limits and the like, but it should be built in and not left to an example that is "not suitable for production." This will prove it's possible to implement a production-quality TaskStore in the easiest case where everything is tracked in-memory and lost on process restart.

  • Note: This is not suitable for production use as all data is lost on restart.
  • For production, consider implementing TaskStore with a database or distributed cache.

This is true, but I think the impact is overstated. The logic in protocol.ts that calls _taskStore.updateTaskStatus(taskMetadata.taskId, 'input_required'); and then sets the task back to 'working' when the a request completes also breaks if the server restarts. If the process exits before the client provides a response, the task will be permanently left in an 'input_required' state indefinitely without some manual intervention outside of the SDK.

In most cases supported by the SDK, when tasks cannot outlive the MCP server process, an in-memory TaskStore would be better than a distributed one because the client will be informed that the Task is no longer being tracked by the server after it restarts automatically.

Copy link
Contributor Author

@LucaButBoring LucaButBoring Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't disagree, but precedence was the main reason for leaving this as an example and leaving out any limits, really - it's the same case with InMemoryEventStore and InMemoryOAuthClientProvider. @ihrpr @felixweinberger any input here?

Copy link
Contributor Author

@LucaButBoring LucaButBoring Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the SDK is going to provide a production-grade implementation, it needs to have limits and some form of logging extension points, but if it is not going to provide a production-grade implementation, I don't want to misrepresent this example as one.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can never implement a production-ready service implementation, because that will always require some additional resources

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems right to me to have this as an interface - it seems hard to provide a generic "production-ready" approach here; I guess we could use sqlite? But I think the argument witht he InMemoryEventStore and InMemoryOAuthClientProvider are convincing. If we wanted to create production ready examples, we can do that as follow-ups.

However, it's important this is documented clearly, which it is in the README.

@LucaButBoring
Copy link
Contributor Author

LucaButBoring commented Oct 31, 2025

It still might be nice to have an end-to-end test demonstrating that client-side support for stuff like elicitations works using the public APIs.

Agreed, will add this.

edit: Done

@LucaButBoring
Copy link
Contributor Author

Since I can use a distributed queue, why not just implement the transport between the server and client based on the distributed queue?

Can you clarify what you mean by this part?

If you mean that you would put the queue directly between the client and server, rather than having it within the server, that sounds like a custom transport, if I'm interpreting it correctly. A custom transport that handles this would work, but we do need something for sHTTP, and I'd prefer not to make the sHTTP transport have special logic for particular JSON-RPC requests if that can be avoided. The message queue here is transport-agnostic.

Specifically, if this were pushed into the transport, sHTTP would need to handle this by:

  1. Checking related-task to determine if it needs to queue a message to the client
  2. Checking if a request is a tasks/result request, and if so, dequeueing all messages into the response stream

Both of these things sound like they should be the responsibility of Protocol, to me. Protocol is aware of there being request/response streams, but doesn't know what the transport is — likewise, the transport knows how the request/response streams are implemented, but doesn't know how the underlying protocol operations need to be handled (with the exception of initialize, unfortunately).

Copy link
Contributor

@felixweinberger felixweinberger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @LucaButBoring for implementing this!

I think my primary feedback is that the README needs to be updated / expanded to explain all the APIs we expect people to use. This could be done in a follow-up though.

I also tried to test the examples but ran into some issues here, because of a discrepancy between taskId creation on server / client, because InMemoryTaskStore ignores the taskId being created:

// Generate a simple task ID (in production, use a more secure method)
const taskId = `task-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;

And just uses the generateTaskId() result instead:

const taskId = this.generateTaskId();

So I think if we fix those 2 things this is good to go from my side. Thanks for adding a whopping 11k+ lines of test coverage for this feature!

@LucaButBoring
Copy link
Contributor Author

@felixweinberger Updated the example and README, let me know what you think 😄

I also simplified the RequestTaskStore interface a bit more by removing the (also now-redundant) requestId and request parameters, since the RequestTaskStore shim automatically passes those to TaskStore. I missed a few things like that when I first created that wrapper and moved common logic into it, but I think I got everything now.

Finally, I added lastUpdatedAt to Task, which is the final revision accepted to the spec (we're doing a hard cutoff here): modelcontextprotocol/modelcontextprotocol#1875

@LucaButBoring
Copy link
Contributor Author

Updated @maxisbey

src/types.ts Outdated
Comment on lines 676 to 678
export const TaskStatusNotificationParamsSchema = z.object({
task: TaskSchema
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was testing interplay between a python server <> typescript client and vice versa.

  1. python server <> typescript client worked
Image
  1. python client <> typescript server ran into some validation issues because of this

I think this should actually be:

export const TaskStatusNotificationParamsSchema = NotificationsParamsSchema.merge(TaskSchema);

Because the spec doesn't nest task in the notification: https://modelcontextprotocol.io/specification/draft/schema#taskstatusnotificationparams

TaskStatusNotificationParams: NotificationParams & Task

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in latest commit

@felixweinberger
Copy link
Contributor

felixweinberger commented Nov 25, 2025

@maxisbey @LucaButBoring I tested TS <> Py. The following worked:

  1. TS Client -> TS Server
  2. Py Client -> Py Server
  3. TS Client -> Py Server

But this didn't work:

  1. Py Server -> TS Client

To make this work cleanly I had to make 2 small changes:

Then I get this:

CleanShot 2025-11-25 at 18 33 18

@LucaButBoring
Copy link
Contributor Author

TS SDK fix is in: 681cf0d

@felixweinberger
Copy link
Contributor

TS SDK fix is in: 681cf0d

Gonna test elicitation examples as well, assuming that works fine I think we should get this landed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement SEP-1686: Tasks

6 participants